/*
 * Copyright 2022 André Claßen
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.burnoutcrew.reorderable

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshotFlow
import androidx.compose.runtime.withFrameMillis
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.util.fastForEach
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.Job
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import kotlin.math.absoluteValue
import kotlin.math.min
import kotlin.math.sign

/**
 * 可重排序状态抽象类，用于管理可拖拽排序列表的状态。
 *
 * @param T 列表项类型
 * @param scope 协程作用域，用于启动协程任务
 * @param maxScrollPerFrame 每帧最大滚动距离
 * @param onMove 当元素被移动时的回调函数，接收起始位置和目标位置两个参数
 * @param canDragOver 判断是否可以将一个元素拖拽到另一个元素上的条件判断函数（可选）
 * @param onDragStart 拖拽开始时的回调函数，接收起始索引及触摸点坐标（可选）
 * @param onDragEnd 拖拽结束时的回调函数，接收起始索引与结束索引作为参数
 * @param dragCancelledAnimation 拖拽取消时执行的动画效果
 */
abstract class ReorderableState<T>(
    private val scope: CoroutineScope,
    private val maxScrollPerFrame: Float,
    private val onMove: (fromIndex: ItemPosition, toIndex: ItemPosition) -> (Unit),
    private val canDragOver: ((draggedOver: ItemPosition, dragging: ItemPosition) -> Boolean)?,
    private val onDragStart: ((startIndex: Int, x: Int, y: Int) -> (Unit))? = null,
    private val onDragEnd: ((startIndex: Int, endIndex: Int) -> (Unit))?,
    val dragCancelledAnimation: DragCancelledAnimation
) {
    /**
     * 布局窗口在屏幕中的偏移量
     */
    var layoutWindowPosition = mutableStateOf(Offset.Zero)

    /**
     * 正在被拖拽的项目索引
     */
    var draggingItemIndex by mutableStateOf<Int?>(null)
        private set

    /**
     * 获取正在被拖拽项目的键值标识符
     */
    val draggingItemKey: Any?
        get() = selected?.itemKey

    /**
     * 抽象属性：获取指定项左侧边界坐标
     */
    protected abstract val T.left: Int

    /**
     * 抽象属性：获取指定项顶部边界坐标
     */
    protected abstract val T.top: Int

    /**
     * 抽象属性：获取指定项右侧边界坐标
     */
    protected abstract val T.right: Int

    /**
     * 抽象属性：获取指定项底部边界坐标
     */
    protected abstract val T.bottom: Int

    /**
     * 抽象属性：获取指定项宽度
     */
    protected abstract val T.width: Int

    /**
     * 抽象属性：获取指定项高度
     */
    protected abstract val T.height: Int

    /**
     * 抽象属性：获取指定项在其数据源中的索引
     */
    protected abstract val T.itemIndex: Int

    /**
     * 抽象属性：获取指定项的唯一键值
     */
    protected abstract val T.itemKey: Any

    /**
     * 抽象属性：当前可见的所有项目信息列表
     */
    protected abstract val visibleItemsInfo: List<T>

    /**
     * 抽象属性：第一个可见项目的索引
     */
    protected abstract val firstVisibleItemIndex: Int

    /**
     * 抽象属性：第一个可见项目相对于视口的滚动偏移量
     */
    protected abstract val firstVisibleItemScrollOffset: Int

    /**
     * 抽象属性：视口起始偏移量
     */
    protected abstract val viewportStartOffset: Int

    /**
     * 抽象属性：视口终止偏移量
     */
    protected abstract val viewportEndOffset: Int

    /**
     * 内部使用的通道，用于发送自动滚动的距离
     */
    internal val scrollChannel = Channel<Float>()

    /**
     * 被拖动项目的左边缘相对位置
     */
    val draggingItemLeft: Float
        get() = if(draggingItemKey!=null) draggingLayoutInfo?.let { item ->
            (selected?.left ?: 0) + draggingDelta.x - item.left
        } ?: 0f else 0f

    /**
     * 被拖动项目的上边缘相对位置
     */
    val draggingItemTop: Float
        get() = if(draggingItemKey!=null) draggingLayoutInfo?.let { item ->
            (selected?.top ?: 0) + draggingDelta.y - item.top
        } ?: 0f else 0f

    /**
     * 是否是垂直方向滚动布局
     */
    abstract val isVerticalScroll: Boolean

    /**
     * 根据当前拖拽索引查找对应的布局信息对象
     */
    private val draggingLayoutInfo: T?
        get() = visibleItemsInfo
            .firstOrNull { it.itemIndex == draggingItemIndex }

    /**
     * 当前拖拽过程中的位移增量
     */
    private var draggingDelta by mutableStateOf(Offset.Zero)

    /**
     * 当前选中并准备拖拽的项目
     */
    private var selected by mutableStateOf<T?>(null)

    /**
     * 自动滚动任务句柄
     */
    private var autoscroller: Job? = null

    /**
     * 存储可能的目标放置区域列表
     */
    private val targets = mutableListOf<T>()

    /**
     * 存储各候选目标与其当前位置之间的距离平方
     */
    private val distances = mutableListOf<Int>()

    /**
     * 抽象方法：滚动至特定索引处，并设置偏移量
     *
     * @param index 目标索引
     * @param offset 偏移量
     */
    protected abstract suspend fun scrollToItem(index: Int, offset: Int)

    /**
     * 创建一个流，当拖拽状态发生变化且有可见项更新时触发通知
     */
    @OptIn(ExperimentalCoroutinesApi::class)
    internal fun visibleItemsChanged() =
        snapshotFlow { draggingItemIndex != null }
            .flatMapLatest { if (it) snapshotFlow { visibleItemsInfo } else flowOf(null) }
            .filterNotNull()
            .distinctUntilChanged { old, new -> old.firstOrNull()?.itemIndex == new.firstOrNull()?.itemIndex && old.count() == new.count() }

    /**
     * 处理拖拽开始事件
     *
     * @param offsetX 触摸点X轴偏移量
     * @param offsetY 触摸点Y轴偏移量
     * @return 是否成功识别出可拖拽项目
     */
    internal open fun onDragStart(offsetX: Int, offsetY: Int): Boolean {
        // 计算实际点击坐标
        val x: Int
        val y: Int
        if (isVerticalScroll) {
            x = offsetX
            y = offsetY + viewportStartOffset
        } else {
            x = offsetX + viewportStartOffset
            y = offsetY
        }

        // 查找点击范围内的项目
        return visibleItemsInfo
            .firstOrNull { x in it.left..it.right && y in it.top..it.bottom }
            ?.also {
                selected = it
                draggingItemIndex = it.itemIndex
                onDragStart?.invoke(it.itemIndex, offsetX, offsetY)
            } != null
    }

    /**
     * 处理拖拽取消事件
     */
    internal fun onDragCanceled() {
        val dragIdx = draggingItemIndex
        if (dragIdx != null) {
            val position = ItemPosition(dragIdx, selected?.itemKey)
            val offset = Offset(draggingItemLeft, draggingItemTop)
            scope.launch {
                dragCancelledAnimation.dragCancelled(position, offset)
            }
        }
        val startIndex = selected?.itemIndex
        val endIndex = draggingItemIndex
        selected = null
        draggingDelta = Offset.Zero
        draggingItemIndex = null
        cancelAutoScroll()
        onDragEnd?.apply {
            if (startIndex != null && endIndex != null) {
                invoke(startIndex, endIndex)
            }
        }
    }

    /**
     * 处理拖拽过程中手指移动事件
     *
     * @param offsetX X轴方向的移动距离
     * @param offsetY Y轴方向的移动距离
     */
    internal fun onDrag(offsetX: Int, offsetY: Int) {
        val selected = selected ?: return
        draggingDelta = Offset(draggingDelta.x + offsetX, draggingDelta.y + offsetY)
        val draggingItem = draggingLayoutInfo ?: return
        val startOffset = draggingItem.top + draggingItemTop
        val startOffsetX = draggingItem.left + draggingItemLeft

        // 寻找合适的放置目标并交换位置
        chooseDropItem(
            draggingItem,
            findTargets(draggingDelta.x.toInt(), draggingDelta.y.toInt(), selected),
            startOffsetX.toInt(),
            startOffset.toInt()
        )?.also { targetItem ->
            if (targetItem.itemIndex == firstVisibleItemIndex || draggingItem.itemIndex == firstVisibleItemIndex) {
                scope.launch {
                    onMove.invoke(
                        ItemPosition(draggingItem.itemIndex, draggingItem.itemKey),
                        ItemPosition(targetItem.itemIndex, targetItem.itemKey)
                    )
                    scrollToItem(firstVisibleItemIndex, firstVisibleItemScrollOffset)
                }
            } else {
                onMove.invoke(
                    ItemPosition(draggingItem.itemIndex, draggingItem.itemKey),
                    ItemPosition(targetItem.itemIndex, targetItem.itemKey)
                )
            }
            draggingItemIndex = targetItem.itemIndex
        }

        // 启动或停止自动滚动逻辑
        with(calcAutoScrollOffset(0, maxScrollPerFrame)) {
            if (this != 0f) autoscroll(this)
        }
    }

    /**
     * 执行自动滚动操作
     *
     * @param scrollOffset 需要滚动的距离
     */
    private fun autoscroll(scrollOffset: Float) {
        if (scrollOffset != 0f) {
            if (autoscroller?.isActive == true) {
                return
            }
            autoscroller = scope.launch {
                var scroll = scrollOffset
                var start = 0L
                while (scroll != 0f && autoscroller?.isActive == true) {
                    withFrameMillis {
                        if (start == 0L) {
                            start = it
                        } else {
                            scroll = calcAutoScrollOffset(it - start, maxScrollPerFrame)
                        }
                    }
                    scrollChannel.trySend(scroll)
                }
            }
        } else {
            cancelAutoScroll()
        }
    }

    /**
     * 取消正在进行的自动滚动任务
     */
    private fun cancelAutoScroll() {
        autoscroller?.cancel()
        autoscroller = null
    }

    /**
     * 在给定区域内寻找所有潜在的放置目标
     *
     * @param x 拖拽中心点X坐标
     * @param y 拖拽中心点Y坐标
     * @param selected 当前被拖拽的对象
     * @return 排序后的候选目标列表
     */
    protected open fun findTargets(x: Int, y: Int, selected: T): List<T> {
        targets.clear()
        distances.clear()
        val left = x + selected.left
        val right = x + selected.right
        val top = y + selected.top
        val bottom = y + selected.bottom
        val centerX = (left + right) / 2
        val centerY = (top + bottom) / 2

        visibleItemsInfo.fastForEach { item ->
            if (
                item.itemIndex == draggingItemIndex
                || item.bottom < top
                || item.top > bottom
                || item.right < left
                || item.left > right
            ) {
                return@fastForEach
            }

            if (canDragOver?.invoke(
                    ItemPosition(item.itemIndex, item.itemKey),
                    ItemPosition(selected.itemIndex, selected.itemKey)
                ) != false
            ) {
                val dx = (centerX - (item.left + item.right) / 2).absoluteValue
                val dy = (centerY - (item.top + item.bottom) / 2).absoluteValue
                val dist = dx * dx + dy * dy
                var pos = 0
                for (j in targets.indices) {
                    if (dist > distances[j]) {
                        pos++
                    } else {
                        break
                    }
                }
                targets.add(pos, item)
                distances.add(pos, dist)
            }
        }
        return targets
    }

    /**
     * 从候选目标中选择最合适的放置位置
     *
     * @param draggedItemInfo 被拖拽项目的布局信息
     * @param items 候选目标列表
     * @param curX 当前拖拽位置X坐标
     * @param curY 当前拖拽位置Y坐标
     * @return 最佳匹配的目标项
     */
    protected open fun chooseDropItem(draggedItemInfo: T?, items: List<T>, curX: Int, curY: Int): T? {
        if (draggedItemInfo == null) {
            return if (draggingItemIndex != null) items.lastOrNull() else null
        }
        var target: T? = null
        var highScore = -1
        val right = curX + draggedItemInfo.width
        val bottom = curY + draggedItemInfo.height
        val dx = curX - draggedItemInfo.left
        val dy = curY - draggedItemInfo.top

        items.fastForEach { item ->
            if (dx > 0) {
                val diff = item.right - right
                if (diff < 0 && item.right > draggedItemInfo.right) {
                    val score = diff.absoluteValue
                    if (score > highScore) {
                        highScore = score
                        target = item
                    }
                }
            }
            if (dx < 0) {
                val diff = item.left - curX
                if (diff > 0 && item.left < draggedItemInfo.left) {
                    val score = diff.absoluteValue
                    if (score > highScore) {
                        highScore = score
                        target = item
                    }
                }
            }
            if (dy < 0) {
                val diff = item.top - curY
                if (diff > 0 && item.top < draggedItemInfo.top) {
                    val score = diff.absoluteValue
                    if (score > highScore) {
                        highScore = score
                        target = item
                    }
                }
            }
            if (dy > 0) {
                val diff = item.bottom - bottom
                if (diff < 0 && item.bottom > draggedItemInfo.bottom) {
                    val score = diff.absoluteValue
                    if (score > highScore) {
                        highScore = score
                        target = item
                    }
                }
            }
        }
        return target
    }

    /**
     * 计算超出视图边界的自动滚动偏移量
     *
     * @param time 已持续时间毫秒数
     * @param maxScroll 最大允许滚动速度
     * @return 应该进行的滚动距离
     */
    private fun calcAutoScrollOffset(time: Long, maxScroll: Float): Float {
        val draggingItem = draggingLayoutInfo ?: return 0f
        val startOffset: Float
        val endOffset: Float
        val delta: Float
        if (isVerticalScroll) {
            startOffset = draggingItem.top + draggingItemTop
            endOffset = startOffset + draggingItem.height
            delta = draggingDelta.y
        } else {
            startOffset = draggingItem.left + draggingItemLeft
            endOffset = startOffset + draggingItem.width
            delta = draggingDelta.x
        }
        return when {
            delta > 0 ->
                (endOffset - viewportEndOffset).coerceAtLeast(0f)
            delta < 0 ->
                (startOffset - viewportStartOffset).coerceAtMost(0f)
            else -> 0f
        }
            .let { interpolateOutOfBoundsScroll((endOffset - startOffset).toInt(), it, time, maxScroll) }
    }

    companion object {
        /**
         * 加速限制的最大时间间隔（单位：毫秒）
         */
        private const val ACCELERATION_LIMIT_TIME_MS: Long = 1500

        /**
         * 缓出四次方插值器函数
         */
        private val EaseOutQuadInterpolator: (Float) -> (Float) = {
            val t = 1 - it
            1 - t * t * t * t
        }

        /**
         * 缓入五次方插值器函数
         */
        private val EaseInQuintInterpolator: (Float) -> (Float) = {
            it * it * it * it * it
        }

        /**
         * 插值计算越界滚动量
         *
         * @param viewSize 视图尺寸大小
         * @param viewSizeOutOfBounds 越界部分长度
         * @param time 当前已持续的时间
         * @param maxScroll 最大滚动速度
         * @return 实际应滚动的距离
         */
        private fun interpolateOutOfBoundsScroll(
            viewSize: Int,
            viewSizeOutOfBounds: Float,
            time: Long,
            maxScroll: Float,
        ): Float {
            if (viewSizeOutOfBounds == 0f) return 0f
            val outOfBoundsRatio = min(1f, 1f * viewSizeOutOfBounds.absoluteValue / viewSize)
            val cappedScroll = sign(viewSizeOutOfBounds) * maxScroll * EaseOutQuadInterpolator(outOfBoundsRatio)
            val timeRatio = if (time > ACCELERATION_LIMIT_TIME_MS) 1f else time.toFloat() / ACCELERATION_LIMIT_TIME_MS
            return (cappedScroll * EaseInQuintInterpolator(timeRatio)).let {
                if (it == 0f) {
                    if (viewSizeOutOfBounds > 0) 1f else -1f
                } else {
                    it
                }
            }
        }
    }
}
